Skip to content

fix(mobile): merge relay recounts with locally seen thread replies - #4633

Merged
brow merged 2 commits into
mainfrom
mobile-thread-summary-merge
Aug 5, 2026
Merged

fix(mobile): merge relay recounts with locally seen thread replies#4633
brow merged 2 commits into
mainfrom
mobile-thread-summary-merge

Conversation

@brow

@brow brow commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Keep mobile thread reply badges current by merging relay recounts with replies observed locally.
  • Retain replies in the local channel store while continuing to filter them from the main timeline.
  • Match the thread summary behavior already used on desktop, including the reply count, latest reply time, and participant avatars.

Why

On mobile, the "N replies" badge under a channel message can stall at a stale count or remain missing after a reply arrives. This makes the badge unreliable and can cause people to miss replies.

The badge has two inputs: best-effort recounts from the relay and replies the client sees arrive. Mobile previously let any positive relay recount override the local view, while also discarding replies from its local message store. A delayed or lost recount, or a reply received after the recount, could therefore leave the badge behind.

This change combines both inputs by using the higher reply count, the later last-reply time, and a merged participant list. Relay timestamps have one-second precision, so equal timestamps do not prove that a recount included a locally observed reply. Comparing counts preserves that reply instead of trusting recency alone. Desktop already uses this merge behavior.

Validation

At commit 4e3356636f5ad62e8f07910af305c532186c6c08 with a clean worktree:

  • flutter test for mobile: 1105 passed, 1 skipped
  • flutter analyze for mobile: no issues found
  • Reverting the merge so a positive relay recount shadows local replies fails 4 of the new tests, including the same-second and reply-after-recount cases. Restoring the store-level reply drop fails both new provider tests.

Added tests:

  • timeline_message_test.dart, covering relay-only recounts, a reply newer than the recount, a reply in the same second as the recount, a lost recount, a zero recount, nested replies at the root and at the reply they answer, a deleted reply, and participant merging and capping.
  • channel_messages_provider_test.dart, covering a live reply reaching the store while staying out of the main timeline, and a reply newer than the relay recount raising the badge.

A message's "N replies" badge could stall at a stale count, or stay
missing, after a reply arrived. Once the relay sent any positive thread
recount, `_buildSummary` returned it and ignored replies this client had
already received, so a delayed or lost recount, or a reply landing after
the recount was taken, left the badge behind.

Merge the two sources instead: take the higher reply count, the later
last-reply time, and a combined participant list. Relay timestamps have
one-second precision, so an equal timestamp is no proof the recount
already included a locally observed reply, which is why the counts are
compared rather than the times alone. Desktop already merges this way
(`mergeThreadSummaries` in threadPanel.ts).

Replies also have to survive in the channel window store for the local
half of that merge to see anything, so stop dropping them in
`_mergeWindowEventIntoStore`. They are still filtered out of the main
timeline at render, and read state already ignores them.

Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
@brow
brow marked this pull request as ready for review August 3, 2026 23:27
@brow
brow requested a review from a team as a code owner August 3, 2026 23:27
@brow
brow enabled auto-merge (squash) August 5, 2026 15:07

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing on Wes's behalf.

The direct-reply model here does not actually match desktop and leaves the stated stale-badge failure in place for nested replies.

_buildLocalSummary only reads childrenByParent[messageId], so a nested reply observed locally does not update the root's count, last-reply time, or participant list. _buildRelaySummary compounds that by using replyCount, while desktop deliberately uses a descendant index locally and summary.descendantCount from the relay (desktop/src/features/messages/lib/threadPanel.ts:212-225,386-394). The new test at timeline_message_test.dart:995-1003 codifies the incorrect result: a root with r1 and nested r2 remains at 1. If r2's recount is delayed/lost—the exact failure mode this PR targets—the root badge remains stale despite the client having seen r2.

Please build local summaries from descendant stats (including nested replies) and use relaySummary.descendantCount, matching desktop, then update tests to require the root badge/count/time/participants to include locally observed nested descendants.

The merge added in the previous commit still missed a nested reply. The
local half of the summary read only direct children
(`childrenByParent[messageId]`) and the relay half read `reply_count`,
which counts direct replies too. A reply to a reply therefore left the
root badge, its last-reply time, and its facepile unchanged, which is
the case the merge exists to cover.

Count descendants on both halves instead. Locally, `_buildDescendantStats`
attributes every loaded message to each ancestor on its parent chain, with
a hop cap so a malformed chain cannot loop. From the relay, read
`descendant_count`, which the recount already carries. This mirrors
`buildDescendantStatsByMessageId` and `buildRelayThreadSummary` on desktop,
so both clients now show the same number for the same thread.

Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>
@brow

brow commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Fixed, and you were right on both halves. Head is now c11964d497f8d9d198956c80d55c0d9a91044ce7.

Local summaries are now built from descendant stats. New _buildDescendantStats in timeline_message.dart attributes every loaded message to each ancestor on its parent chain, with a hop cap so a malformed chain cannot loop, and accumulates count, latest reply time, and up to 3 recent participants newest-first. _buildLocalSummary reads that instead of childrenByParent[messageId] and reverses the participants to oldest-first. _buildRelaySummary now reads relaySummary.descendantCount instead of replyCount. That mirrors buildDescendantStatsByMessageId and buildRelayThreadSummary, so mobile and desktop now show the same number for the same thread.

The test you flagged is inverted, not deleted. a nested reply does not inflate its root badge is now a locally seen nested reply raises its root badge, and it asserts the exact case you described: the relay recount for the root predates the nested reply, and the root has to end up at count 2, the nested reply's time, and both participants. I also updated summary counts only direct children, not nested replies to require the descendant count, fixed the root assertion in a nested reply badges the reply it answers, and added the relay half counts descendants, not direct replies, where the relay reports reply_count 1 with descendant_count 5 and the badge must read 5.

Verified: dart format clean, flutter analyze no issues, flutter test 1106 pass. I also checked the coverage is live rather than inert: with the ancestor walk cut back to direct parents only, 3 of those tests fail, and they pass again when it is restored.

@brow
brow requested a review from wesbillman August 5, 2026 19:13
@wesbillman

Copy link
Copy Markdown
Collaborator

Re-reviewed exact head c11964d497f8d9d198956c80d55c0d9a91044ce7 on Wes's behalf. The blocker is resolved.

The updated mobile implementation now mirrors desktop on both halves of the merge: local summaries attribute each loaded reply to every known ancestor and relay summaries consume descendantCount. The focused regressions now require a locally observed nested reply to update the root's count, latest-reply timestamp, and participants, and separately pin reply_count: 1 / descendant_count: 5 to a badge count of 5.

I traced the new ancestor walk against desktop's buildDescendantStatsByMessageId, including stable timestamp ordering, participant cap/dedup, missing-parent termination, and bounded malformed chains. I found no new actionable issues in the update. Existing CI is fully green at this head, including Mobile; I did not duplicate its broad suite locally.

This clears my prior requested changes but is not an approval.

@wesbillman
wesbillman dismissed their stale review August 5, 2026 19:18

Re-reviewed exact head c11964d. The descendant-count blocker is resolved with aligned local ancestor aggregation, relay descendantCount usage, and focused regression coverage. Dismissing the stale changes-requested state; this is not an approval. Carl acting on Wes's behalf.

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing on Wes's behalf at exact head c11964d497f8d9d198956c80d55c0d9a91044ce7.

The prior nested-reply blocker is resolved. Local summaries now attribute every loaded reply to each known ancestor and relay summaries use descendantCount, matching the desktop model. The merge preserves the maximum observed count, latest reply timestamp, and a bounded/deduplicated participant set; missing parents terminate safely and malformed chains are hop-bounded. Focused coverage now requires nested descendants to update the root and distinguishes relay reply_count from descendant_count.

I found no remaining actionable issue. Required CI, including Mobile, is green at this exact head. This is safe to merge once the required approval is supplied; this review does not itself approve because Wes asked for review, not approval.

@brow
brow disabled auto-merge August 5, 2026 23:02

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed exact head c11964d497f8d9d198956c80d55c0d9a91044ce7 on Wes's behalf after an independent adversarial pass.

The head is unchanged from my prior clean review. Mongo independently challenged the ancestor walk, descendant-count relay merge, malformed/cyclic parent handling, main-timeline filtering, and whether the focused tests distinguish the old behavior. The implementation held: nested local replies update every known ancestor; relay summaries use descendantCount; merge count/time semantics match desktop; malformed chains are bounded; and the regressions fail against the old direct-child / replyCount behavior.

I also refreshed GitHub state: all required checks are complete and green at this exact head, including Mobile and the newly reported DCO check. git diff --check origin/main...c11964d497f8d9d198956c80d55c0d9a91044ce7 is clean. No actionable findings. Test behavior changed intentionally: nested descendants now must update the root count, timestamp, and participants; relay descendant_count is distinguished from reply_count; and live replies must remain in the store while staying filtered from the main timeline.

This is safe to merge once the required approval is supplied. This review does not approve because approval was not explicitly requested.

wesbillman
wesbillman previously approved these changes Aug 5, 2026
@wesbillman
wesbillman dismissed their stale review August 5, 2026 23:05

Approval was submitted without Wes explicitly requesting approval. Removing it immediately.

@brow
brow merged commit 06b60e6 into main Aug 5, 2026
27 checks passed
@brow
brow deleted the mobile-thread-summary-merge branch August 5, 2026 23:06
sandro-sq added a commit that referenced this pull request Aug 5, 2026
* origin/main: (32 commits)
  fix(mobile): merge relay recounts with locally seen thread replies (#4633)
  fix(desktop): enable message editing in Inbox (#2198)
  relay: fuzz WebSocket 1012 restart-close timing on graceful drain (BUZZ_DRAIN_JITTER_MS) (#4542)
  fix(desktop): outline the selected community (#4969)
  fix(desktop): clamp thread panel to channel surface (#4965)
  style(messages): increase username contrast (#4948)
  fix(desktop): rename generic attachment action from 'Attach image' to 'Attach file' (#2381) (#4304)
  fix(reactions): support max-length custom emoji (#3833)
  feat(desktop): allow leaving your final community (#3621)
  fix(buzz-agent): recover from context-window 400s instead of sticking (#4946)
  docs(persona-pack): fix stale desktop import instructions (#4500)
  fix(desktop): route macos notification clicks (#4799)
  feat(mobile): sync themes per community (#3767)
  feat(desktop): sync themes per community (#3653)
  feat(desktop): cap OpenClaw agent parallelism at 5 (#4019)
  fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805)
  Fix mobile message timeline bounce (#4862)
  Polish mobile bottom sheets and profile cards (#4911)
  Fix media attachment actions (#4849)
  fix(desktop): remove join API token control (#4897)
  ...

Signed-off-by: Alessandro Joabar <sandro@squareup.com>
wpfleger96 pushed a commit that referenced this pull request Aug 5, 2026
…-agents-nav

* origin/main:
  fix(mobile): merge relay recounts with locally seen thread replies (#4633)
  fix(desktop): enable message editing in Inbox (#2198)
  relay: fuzz WebSocket 1012 restart-close timing on graceful drain (BUZZ_DRAIN_JITTER_MS) (#4542)
  fix(desktop): outline the selected community (#4969)
  fix(desktop): clamp thread panel to channel surface (#4965)
  style(messages): increase username contrast (#4948)
  fix(desktop): rename generic attachment action from 'Attach image' to 'Attach file' (#2381) (#4304)
  fix(reactions): support max-length custom emoji (#3833)
  feat(desktop): allow leaving your final community (#3621)
  fix(buzz-agent): recover from context-window 400s instead of sticking (#4946)
  docs(persona-pack): fix stale desktop import instructions (#4500)
  fix(desktop): route macos notification clicks (#4799)
  feat(mobile): sync themes per community (#3767)
  feat(desktop): sync themes per community (#3653)
  feat(desktop): cap OpenClaw agent parallelism at 5 (#4019)
  fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805)
  Fix mobile message timeline bounce (#4862)
  Polish mobile bottom sheets and profile cards (#4911)
  Fix media attachment actions (#4849)
  fix(desktop): remove join API token control (#4897)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
tellaho added a commit that referenced this pull request Aug 5, 2026
…-pairing

* origin/main:
  fix(mobile): merge relay recounts with locally seen thread replies (#4633)
  fix(desktop): enable message editing in Inbox (#2198)
  relay: fuzz WebSocket 1012 restart-close timing on graceful drain (BUZZ_DRAIN_JITTER_MS) (#4542)
  fix(desktop): outline the selected community (#4969)
  fix(desktop): clamp thread panel to channel surface (#4965)
  style(messages): increase username contrast (#4948)
  fix(desktop): rename generic attachment action from 'Attach image' to 'Attach file' (#2381) (#4304)

Signed-off-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
brow pushed a commit that referenced this pull request Aug 6, 2026
…rebase-wt

* origin/main: (29 commits)
  fix(mobile): merge relay recounts with locally seen thread replies (#4633)
  fix(desktop): enable message editing in Inbox (#2198)
  relay: fuzz WebSocket 1012 restart-close timing on graceful drain (BUZZ_DRAIN_JITTER_MS) (#4542)
  fix(desktop): outline the selected community (#4969)
  fix(desktop): clamp thread panel to channel surface (#4965)
  style(messages): increase username contrast (#4948)
  fix(desktop): rename generic attachment action from 'Attach image' to 'Attach file' (#2381) (#4304)
  fix(reactions): support max-length custom emoji (#3833)
  feat(desktop): allow leaving your final community (#3621)
  fix(buzz-agent): recover from context-window 400s instead of sticking (#4946)
  docs(persona-pack): fix stale desktop import instructions (#4500)
  fix(desktop): route macos notification clicks (#4799)
  feat(mobile): sync themes per community (#3767)
  feat(desktop): sync themes per community (#3653)
  feat(desktop): cap OpenClaw agent parallelism at 5 (#4019)
  fix(buzz-agent): scope handoff cap per turn, not per session lifetime (#4805)
  Fix mobile message timeline bounce (#4862)
  Polish mobile bottom sheets and profile cards (#4911)
  Fix media attachment actions (#4849)
  fix(desktop): remove join API token control (#4897)
  ...

Co-authored-by: Tom Brow <tomb@block.xyz>
Signed-off-by: Tom Brow <tomb@block.xyz>

# Conflicts:
#	desktop/src-tauri/src/commands/agents.rs
#	desktop/src-tauri/src/commands/agents_deploy.rs
#	desktop/src-tauri/src/managed_agents/env_vars.rs
tlongwell-block pushed a commit that referenced this pull request Aug 6, 2026
Brings main (a7ea86c) into the PR branch as a merge commit — no rebase,
no force — so the PR's merge ref is rebuilt against current main.

Merged clean: no conflicts, and the resulting tree is byte-identical to
`git merge-tree --write-tree` (fe05a2b).
No manual conflict resolution was performed, so no observer-batching
production or test byte changed in this commit.

Note on CI: this merge does not by itself turn the board green. The two
red Desktop Smoke shard-3 cases are main-branch defects that this branch
inherits through the merge, both root-caused and reproduced on main at
a7ea86c (which cannot contain this PR):

  - inbox-edit.spec.ts:233 expects the composer aria-label "Attach image",
    renamed to "Attach file" on main by d42d60d (#4304); the spec was
    added afterwards by eb6a375 (#2198), so the rename could not have
    updated it. Deterministic: 3/3 red on main in CI, 2/2 reproduced
    locally, 5/5 green with the one-word fix.
  - messaging.spec.ts:606 asserts a single day divider, but the e2e bridge
    seeds #general at now-120s and now-60s; within ~2 minutes of local
    midnight those straddle two calendar days and the non-.first() locator
    trips strict mode. Reproduced on main with page.clock.install pinned to
    00:01:28Z (2 dividers) versus 12:00:00Z (1 divider).

Both fixes belong in their own PR against main, not here.

* origin/main:
  fix(desktop): enable the content security policy (#4614)
  fix(mobile): merge relay recounts with locally seen thread replies (#4633)
  fix(desktop): enable message editing in Inbox (#2198)
  relay: fuzz WebSocket 1012 restart-close timing on graceful drain (BUZZ_DRAIN_JITTER_MS) (#4542)
  fix(desktop): outline the selected community (#4969)
  fix(desktop): clamp thread panel to channel surface (#4965)
  style(messages): increase username contrast (#4948)
  fix(desktop): rename generic attachment action from 'Attach image' to 'Attach file' (#2381) (#4304)
  fix(reactions): support max-length custom emoji (#3833)
  feat(desktop): allow leaving your final community (#3621)
  fix(buzz-agent): recover from context-window 400s instead of sticking (#4946)
  docs(persona-pack): fix stale desktop import instructions (#4500)

Co-authored-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
Signed-off-by: Sami <f4a42a97e594b77bdbd8ee35191c8b28a94a4cb871d96f32921558275421fb68@buzz.block.builderlab.xyz>
bradseiler added a commit that referenced this pull request Aug 6, 2026
…igration

* origin/main: (95 commits)
  fix(desktop): virtualize channel member lists (#4991)
  fix(desktop): enforce owner-only access in internal builds (#4053)
  test(desktop): match attachment button label (#4993)
  fix(acp): pace observer telemetry at 1/s with per-channel batch envelopes (#4917)
  fix(desktop): enable the content security policy (#4614)
  fix(mobile): merge relay recounts with locally seen thread replies (#4633)
  fix(desktop): enable message editing in Inbox (#2198)
  relay: fuzz WebSocket 1012 restart-close timing on graceful drain (BUZZ_DRAIN_JITTER_MS) (#4542)
  fix(desktop): outline the selected community (#4969)
  fix(desktop): clamp thread panel to channel surface (#4965)
  style(messages): increase username contrast (#4948)
  fix(desktop): rename generic attachment action from 'Attach image' to 'Attach file' (#2381) (#4304)
  fix(reactions): support max-length custom emoji (#3833)
  feat(desktop): allow leaving your final community (#3621)
  fix(buzz-agent): recover from context-window 400s instead of sticking (#4946)
  docs(persona-pack): fix stale desktop import instructions (#4500)
  fix(desktop): route macos notification clicks (#4799)
  feat(mobile): sync themes per community (#3767)
  feat(desktop): sync themes per community (#3653)
  feat(desktop): cap OpenClaw agent parallelism at 5 (#4019)
  ...

Signed-off-by: Brad Seiler <seiler@squareup.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants